package com.riven.redisson.concurrent;

import com.riven.redisson.annotation.DistributedLock;
import com.riven.redisson.exception.QpsOverLimitException;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.Ordered;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;

/**
 * @author riven
 * @since 2025-05-22
 */
@Slf4j
@Aspect
public class DistributedLockInterceptor implements Ordered, ApplicationContextAware {

    @Setter
    private String application = "application";

    @Setter
    private int order;

    private LockProvider lockProvider;

    private final LockSubjectResolver lockSubjectResolver;

    private final Map<Method, DistributedLock> annotationCache = new ConcurrentHashMap<>();

    public DistributedLockInterceptor(@Autowired(required = false) LockSubjectProvider globalLockSubjectProvider) {
        LockSubjectResolverComposite resolverComposite = new LockSubjectResolverComposite();
        resolverComposite.addResolvers(new AnnotatedParameterLockSubjectResolver());
        resolverComposite.addResolvers(new CustomizedProviderLockSubjectResolver());
        resolverComposite.addResolvers(new DefaultProviderLockSubjectResolver(globalLockSubjectProvider));
        this.lockSubjectResolver = resolverComposite;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        final String[] beanNamesForType = applicationContext.getBeanNamesForType(LockProvider.class);
        if (beanNamesForType.length == 0) {
            throw new BeanInitializationException("no LockProvider found");
        }
        final List<String> customizedList = Arrays.stream(beanNamesForType)
                .filter(item -> !item.equals(DistributedLockBeanDefinitionRegistrar.DEFAULT_LOCK_PROVIDER_BEAN_NAME))
                .toList();
        final int customizedCount = customizedList.size();
        if (customizedCount > 1) {
            throw new BeanInitializationException("customized LockProvider expected one but found " + customizedCount);
        }
        if (customizedCount == 1) {
            this.lockProvider = applicationContext.getBean(customizedList.get(0), LockProvider.class);
            return;
        }
        this.lockProvider = applicationContext.getBean(DistributedLockBeanDefinitionRegistrar.DEFAULT_LOCK_PROVIDER_BEAN_NAME, LockProvider.class);
    }

    @Override
    public int getOrder() {
        return this.order;
    }

    @Pointcut("@annotation(com.riven.redisson.annotation.DistributedLock)")
    public void lockPointCut() {

    }

    @Around("lockPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object resolved = lockSubjectResolver.resolve(joinPoint);
        final String subject = resolved == null ? null : resolved.toString();
        if (!StringUtils.hasText(subject)) {
            return joinPoint.proceed();
        }
        final Signature signature = joinPoint.getSignature();
        String signatureMd5 = DigestUtils.md5DigestAsHex(signature.toLongString().getBytes(StandardCharsets.UTF_8));
        final Lock lock = lockProvider.getLock(String.format("%s#%s@%s", application, signatureMd5, subject));
        final boolean acquired = lock.tryLock();
        if (!acquired) {
            DistributedLock annotation = getAnnotation(((MethodSignature) signature).getMethod());
            throw new QpsOverLimitException(annotation.errorMsg());
        }
        try {
            return joinPoint.proceed();
        } finally {
            lock.unlock();
        }
    }

    private DistributedLock getAnnotation(Method method) {
        return annotationCache.computeIfAbsent(method, m -> m.getDeclaredAnnotation(DistributedLock.class));
    }

}
